package ui;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.FileAppender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;

import utils.ClientConnectionUtil;
import utils.CommonUtil;

/**
 * This class provides the user interface for a simple client-server
 * communication
 * 
 * @author Cipri Lucaci
 * @author Rustem Bekmukhametov
 * @author Alba Xhani
 */

public class Application {
	private static Logger logger = LogManager.getLogger(Application.class.getName()); // Logger object

	private ClientConnectionUtil connectionUtil; // Connection object

	/**
	 * This method creates an application object and starts it
	 * 
	 * @param args
	 * @return
	 */
	public static void main(String[] args) {
		Application app = new Application();
		app.startApplication();
	}

	/**
	 * This method launches the application and tries to read commands entered
	 * by the user
	 * 
	 * @param Nothing
	 * @return true
	 * @exception prints
	 *                out an error message if cannot execute readCommands()
	 */
	public boolean startApplication() {
		initLogger();
		logger.info("Application launched");
		connectionUtil = new ClientConnectionUtil();
		try {
			readCommands();
		} catch (IOException exc) {
			String errorMessage = "an error in readCommands(): ";
			logger.error(errorMessage, exc);
			System.out.println(errorMessage);
		}
		return true;
	}

	/**
	 * This method prompts the user to send a command, reads the command, splits
	 * it in tokens (words) and based on the first token calls the appropriate
	 * method assigned to the command
	 * 
	 * @throws IOException
	 *             when cannot read the command
	 */
	private void readCommands() throws IOException {

		BufferedReader cons = new BufferedReader(new InputStreamReader(
				System.in));
		boolean quit = false;
		while (!quit) {
			System.out.print("EchoClient>");
			String input = "";
			try {
				input = cons.readLine();
			} catch (IOException e) {
				logger.error("An error catched during user input: ", e);
				e.printStackTrace();
			}
			String[] tokens = input.trim().split("\\s+");
			logger.info("user input: " + input.trim());

			if (tokens[0].equals(Constants.COMMANDS.CONNECT)) {
				if (commandConnect(tokens))
					getServerResponse();
			} else if (tokens[0].equals(Constants.COMMANDS.SEND)) {
				if (commandSend(input))
					getServerResponse();
			} else if (tokens[0].equals(Constants.COMMANDS.DISCONNECT)) {
				if (commandDisconnect()) {
					System.out.println("Disconnected successfully!");
				}
				else{
					System.out.println("No active connection");
				}
			} else if (tokens[0].equals(Constants.COMMANDS.LOG_LEVEL)) {
				if (commandLogLevel(tokens))
					getServerResponse();
			} else if (tokens[0].equals(Constants.COMMANDS.HELP)) {
				commandHelp(tokens);
			} else if (tokens[0].equals(Constants.COMMANDS.QUIT)) {
				quit = commandQuit(tokens);
			} else {
				commandUnknown(tokens);
			}
		}
	}

	/**
	 * This method gets the server response and converts it into ASCII format
	 * 
	 * @exception IOException
	 *                when cannot connect successfully with the server
	 */
	private void getServerResponse() {
		byte[] responseBytes;
		String response = "";
		try {
			responseBytes = connectionUtil.receive();
			response = new String(responseBytes, "US-ASCII");
		} catch (IOException exc) {
			logger.error("An error occured while getting response from the server", exc);
			System.out.print(Constants.ERRORS.CONNECTION_NOT_SUCCEEDED);
			return;
		}
		System.out.print("EchoServer>"+response);
	}

	/**
	 * This method checks for the correctness of the server ip and port sent
	 * from the user
	 * 
	 * @param user input
	 * @return true if the input matches the defined conditions
	 * @return false if such conditions are not met
	 */
	private boolean checkIpAndPortFormat(String[] tokens) {
		if (tokens.length != 3) // the number of arguments must be 2 (ip and port)
			return false;
		
		if (tokens[1] == null || tokens[2] == null)
			// for a successful connection attempt the user must provide both ip
			// and port number
			return false;
		
		Pattern pattern = Pattern.compile(Constants.SETTINGS.PATTERN);
		Matcher matcher = pattern.matcher(tokens[1]);
		
		if (!matcher.matches()) // the ip format must be: x.x.x.x
			return false;

		String serverIp = tokens[1]; // Constants.SETTINGS.SERVER_IP;
		int serverPort = 0;
		try {
			serverPort = Integer.parseInt(tokens[2]);
		} catch (Exception ex) {
			return false;
		}
		return true;
	}

	/**
	 * This method establishes the connection with the server
	 * 
	 * @param user input
	 * @return true if the input format is ok and the connection succeeds
	 * @return false if the format check fails or if already connected
	 * @exception when
	 *                connection cannot be established
	 */

	private boolean commandConnect(String[] tokens) throws IOException {
		boolean formatOk = checkIpAndPortFormat(tokens);
		if (!formatOk) {
			incorrectConnect();
			return false;
		}
		boolean connection = connectionUtil.isConnectionEstablished();
		if (connection) {
			System.out.println("Warning: Already connected!");
			return false;
		}
		try {
			String serverIp = tokens[1];
			int serverPort = Integer.parseInt(tokens[2]);
			boolean connectionSucceed = connectionUtil.initSocketConnection(serverIp, serverPort);
			if (connectionSucceed) {
				logger.info("Successfully connected to "+serverIp+" :"+serverPort);
				return true;
			} else {
				logger.warn("Unable to connect to "+serverIp+" :"+serverPort);
				System.out.println(Constants.ERRORS.CONNECTION_NOT_SUCCEEDED);
				return false;
			}
		} 
		catch (SocketTimeoutException exc){
			logger.error(Constants.ERRORS.SOCKET_TIMEOUT, exc);
			System.out.println(Constants.ERRORS.SOCKET_TIMEOUT);
			return false;
		}
		catch (Exception exc) {
			logger.error("Caught during socket connection initiation", exc);
			System.out.println(Constants.ERRORS.CONNECTION_NOT_SUCCEEDED);
			return false;
		}
	}

	private void incorrectConnect() {
		System.out.println(Constants.ERRORS.INVALID_ARGUMENT);
		logger.warn("During connection establishing: "+Constants.ERRORS.INVALID_ARGUMENT);
	}

	private boolean commandDisconnect() {
		try {
			if(connectionUtil.isConnectionEstablished()){
				connectionUtil.closeConnection();
				logger.info("successfully disconnected from the host");
				return true;
			}
			return false;
		} catch (IOException exc) {
			logger.error("Caught when tried to close the connection", exc);
		}
		return true;
	}

	/**
	 * Checks the SEND command parameters. The command should start without
	 * whitespace. The message should be in size less than 128kBytes.
	 * 
	 * @param user
	 *            input
	 * @return
	 */
	private boolean checkSendParameters(String input) {
		String inputMessage = input.trim();
		if (inputMessage.length() < 5)
			return false;
		inputMessage = inputMessage.substring(5); // send_
		byte[] inputMessageBytes;
		try {
			inputMessageBytes = inputMessage.getBytes("US-ASCII");
		} catch (UnsupportedEncodingException exc) {
			logger.error("Caught during convering the message to ASCII bytes", exc);
			return false;
		}
		if (inputMessageBytes.length > 128 * 1024) {
			System.out.println("the server accepts no more than 128 kBytes at once");
			return false;
		}
		return true;
	}

	/**
	 * Handles the SEND command. Checks the SEND parameters. If the format is
	 * ok, and the connection is active, sends the user message to the server
	 * 
	 * @param user input
	 * @return
	 * @exception if
	 *                the argument is invalid
	 */
	private boolean commandSend(String input) {
		boolean formatOk = checkSendParameters(input);
		if (!formatOk) {
			incorrectSent();
			logger.warn("Invalied format of the message to send");
			return false;
		}

		if (!connectionUtil.isConnectionEstablished()) {
			System.out.println(Constants.ERRORS.NO_CONNECTION);
			logger.warn(Constants.ERRORS.NO_CONNECTION);
			return false;
		}
		try {
			String inputMessage = input.substring(input.indexOf("send ") + 5);
			byte[] inputMessageBytes = inputMessage.getBytes("US-ASCII");
			connectionUtil.send(inputMessageBytes);
			logger.info(input);
		} catch (Exception exc) {
			logger.error("Caught during converting the message to bytes and sending", exc);
			System.out.println(Constants.ERRORS.INVALID_ARGUMENT);
			return false;
		}
		return true;
	}

	/**
	 * This method prints out an error message when the argument sent by the
	 * user is invalid
	 */
	private void incorrectSent() {
		System.out.println(Constants.ERRORS.INVALID_ARGUMENT);
		logger.warn("During sending:" + Constants.ERRORS.INVALID_ARGUMENT);
	}

	/**
	 * This method handles the command quit. It simply disconnects and
	 * terminates the application
	 * 
	 * @param tokens
	 * @return
	 */
	private boolean commandQuit(String[] tokens) {
		// call disconnect
		commandDisconnect();
		System.out.println(Constants.MESSAGES.APPLICATION_QUIT);
		// TODO: what happens if the disconnect fails? do we have a lost
		// connection
		// TODO: when we quit before connecting
		return true;
	}

	/**
	 * This method handles the command help and prints out the explanation for
	 * every command as requested by the user input
	 * 
	 * @param tokens
	 * @return
	 */
	private boolean commandHelp(String[] tokens) {

		if (tokens.length >= 2) {
			if (tokens[1].equals(Constants.COMMANDS.CONNECT)) {
				System.out.println(Constants.HELP.CONNECT);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.DISCONNECT)) {
				System.out.println(Constants.HELP.DISCONNECT);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.SEND)) {
				System.out.println(Constants.HELP.SEND);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.QUIT)) {
				System.out.println(Constants.HELP.QUIT);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.LOG_LEVEL)) {
				System.out.println(Constants.HELP.LOG_LEVEL);
				return false;
			}
			if (tokens[1].equals(Constants.COMMANDS.HELP)) {
				System.out.println(Constants.HELP.HELP);
				return false;
			}
		}
		System.out.println("Syntax: \n" + Constants.HELP.CONNECT
				+ Constants.HELP.DISCONNECT + Constants.HELP.SEND
				+ Constants.HELP.QUIT + Constants.HELP.HELP
				+ Constants.HELP.LOG_LEVEL);
		return false;
	}

	/**
	 * This method handles the command for setting the log level
	 * 
	 * @param tokens
	 * @exception when
	 *                the argument is invalid
	 */

	private boolean commandLogLevel(String[] tokens) {
		try {
			if(tokens.length < 2){
				throw new Exception("command logLevel <Argument missing>");
			}
			if (tokens[1].trim().equals("-i")) {
				System.out.println("logLevel = " + logger.getLevel());
				return false;
			}
			Level logLevel = CommonUtil.getLoggingLevel(tokens[1]);
			logger.info("logger level is changing from " + logger.getLevel()+" to "+logLevel);
			logger.setLevel(logLevel);
			System.out.println("logging level set to " + logger.getLevel().toString());
		} catch (Exception exc) {
			logger.warn(exc.getMessage());
			System.out.println(Constants.ERRORS.INVALID_ARGUMENT);
			return false;
		}
		return false;
	}

	/**
	 * This method handles the unknown commands, by printing an error message
	 * 
	 * @param tokens
	 * @return
	 */
	private boolean commandUnknown(String[] tokens) {
		logger.warn("Unrecognized command provided");
		System.out.println(Constants.ERRORS.UNRECOGNIZED_COMMAND);
		return false;
	}

	/**
	 * This method initializes the Logger and sets level ALL by default
	 * 
	 * @param
	 * @return
	 * @exception when
	 *                logger cannot initialize
	 */
	private static void initLogger() {
		logger.setLevel(Level.ALL);
		String logDir = "client.log";
		String pattern = "%d{ISO8601} %-5p [%t] %c: %m%n";
		PatternLayout pLayout = new PatternLayout(pattern);
		FileAppender fa;
		try {
			fa = new FileAppender(pLayout, logDir, true);
			logger.addAppender(fa);
			logger.info("Logger and appenders successfully initiated");
		} catch (IOException exc) {
			logger.error("Caught during logger initialization", exc);
			System.out.println("ERROR during logger initialization: " + exc.getMessage().toString());
		}
	}
}
